package main

import (
	"database/sql"
	"errors"
	_ "github.com/go-sql-driver/mysql"
	"github.com/google/uuid"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/schema"
)

// It embeds gorm.Model and overwrites ID to uuid string.
type BaseModel struct {
	gorm.Model
	ID string `gorm:"primarykey"`
}

func (this *BaseModel) BeforeCreate(tx *gorm.DB) (err error) {
	this.ID = uuid.New().String()

	return nil
}

type NewTable struct {
	BaseModel
	Name string
}

type Manufacturer struct {
	gorm.Model
	Name   string
	Brands []Brand
}

type Brand struct {
	gorm.Model
	Name           string
	ManufacturerID int
	Manufacturer   Manufacturer
	Products       []Product
}

type Product struct {
	gorm.Model
	Code       string
	Price      uint
	ReleasedAt sql.NullTime
	BrandID    int
	Brand      Brand
	Tags       []Tag `gorm:"many2many:product_tags;"`
}

type Tag struct {
	gorm.Model
	Name     string
	Products []Product `gorm:"many2many:product_tags;"`
}

func main() {
	const dsn = "dev-user:dev-user@tcp(127.0.0.1:3306)/golang_orm?charset=utf8mb4&parseTime=True&loc=Local"

	gormConfig := gorm.Config{
		NamingStrategy: schema.NamingStrategy{
			TablePrefix:   "op_", // Table name prefix, table for `User` would be `t_users`
			SingularTable: true,  // Uses singular table name, table for `User` would be `user` with this option enabled
		},
		// GORM performs write (create/update/delete) operations run inside a transaction to ensure data consistency,
		// you can disable it during initialization if it is not required, you will gain about 30%+ performance improvement after that.
		//SkipDefaultTransaction: true,
	}

	// Refers https://github.com/go-sql-driver/mysql#dsn-data-source-name for details
	db, err := gorm.Open(mysql.Open(dsn), &gormConfig)

	if err != nil {
		println("Failed to connect database")
	}

	// Migrates the schema
	db.AutoMigrate(
		&Manufacturer{},
		&Brand{},
		&Product{},
		&NewTable{},
	)

	var product Product
	db.Model(&Product{}).Preload("Tags").First(&product, 1)
	product.Tags = product.Tags[1:]
	db.Debug().Model(&Product{}).Select("Tags").Updates(&product)

	println("Done")

	//var manufacturers []Manufacturer
	//var brands []Brand

	// Eager Loading.
	//db.Model(&Brand{}).Preload("Products").Find(&brands)
	//db.Model(&Manufacturer{}).Preload("Brands").Find(&manufacturers)

	//db.Model(&Product{}).First(&product, 1)
	//
	//// Uses Scan function to update the NullTime value.
	//product.ReleasedAt.Scan(time.Now())
	//db.Save(&product)

	//db.Model(&product).Updates(product)

	//db.Model(&product).UpdateColumn("ReleasedAt", time.Now())

	//brand = Brand{}
	//brand.ID = 2
	//
	//product = Product{
	//	Code:  "UIC732-25",
	//	Price: 1400,
	//	Brand: brand,
	//}
	//
	//db.Create(&product)

	// Transaction example.
	//db.Transaction(func(tx *gorm.DB) error {
	//	// do some database operations in the transaction (use 'tx' from this point, not 'db')
	//	if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
	//		// return any error will rollback
	//		return err
	//	}
	//
	//	if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
	//		return err
	//	}
	//
	//	// return nil will commit the whole transaction
	//	return nil
	//})

	println("Done")
}

func Create(db *gorm.DB) {
	var manufacturer Manufacturer
	var brand Brand
	var product Product
	var tags []Tag

	tags = []Tag{
		{Name: "laptop"},
		{Name: "intel"},
		{Name: "nvidia"},
		{Name: "lcd"},
		{Name: "4g"},
	}

	// Batch insert
	// To efficiently insert large number of records, pass a slice to the Create method.
	// GORM will generate a single SQL statement to insert all the data and backfill primary key values.
	db.Create(&tags)

	// You can create Manufacturer then create Brand
	manufacturer = Manufacturer{Name: "HP"}
	db.Create(&manufacturer)

	// This manufacturer has been created with id returned. It would not be duplicated.
	brand = Brand{Name: "Pavilion", Manufacturer: manufacturer}
	db.Create(&brand)

	product = Product{Code: "840G", Price: 4500, Brand: brand, Tags: tags}
	db.Create(&product)

	// This manufacturer is not created then it will be created with brand together.
	manufacturer = Manufacturer{Name: "Google"}
	brand = Brand{Name: "Pixel", Manufacturer: manufacturer}
	product = Product{Code: "450", Price: 1250, Brand: brand, Tags: tags}
	db.Create(&product)

	manufacturer = Manufacturer{Name: "Microsoft"}
	brand = Brand{Name: "Hololens", Manufacturer: manufacturer}
	product = Product{Code: "V2", Price: 9000, Brand: brand, Tags: tags}
	db.Create(&product)

	// Create
	//result := db.Create(&product)
	//println(result.RowsAffected)
}

// IMPORTANT!!!
// The name used in where condition is database column name, NOT the structure field name.
// Example: code = ?. Not Code = ?.
func Query(db *gorm.DB) {
	var product Product
	var products []Product

	//if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil {
	//	// error handling...
	//}

	// Finds product with integer primary key.
	// SELECT * FROM products WHERE id = 1;
	result := db.First(&product, 1)

	// Returns found records count.
	println(result.RowsAffected)

	// Checks error ErrRecordNotFound
	errors.Is(result.Error, gorm.ErrRecordNotFound)

	// You cannot use the same variable as parameter for next query.
	// If this variable already contains retrieved data it would affect next query condition.
	product = Product{}

	// Gets the first record ordered by primary key.
	// SELECT * FROM products WHERE code = 'D42' ORDER BY id LIMIT 1;
	// If the product already contains data the sql will be:
	// SELECT * FROM `products` WHERE code = 'D44' AND `products`.`deleted_at` IS NULL AND `products`.`id` = 4 LIMIT 1
	// The id will be also used as condition even you did not specify.
	db.First(&product, "code = ?", "D44")

	product = Product{}

	// Gets one record, no specified order.
	// SELECT * FROM products WHERE code = 'D42' LIMIT 1;
	db.Take(&product, "code = ?", "D42")

	// Gets with id of uuid string.
	var newTable = NewTable{BaseModel: BaseModel{ID: "aa3669bd-dff9-4448-b1c8-dde2c6392ec4"}}

	db.Model(&NewTable{}).Take(&newTable)

	newTable = NewTable{}
	db.Model(&NewTable{}).Take(&newTable, "id = ?", "aa3669bd-dff9-4448-b1c8-dde2c6392ec4")

	// Complicated where condition.
	db.Model(&NewTable{}).Where(`
		name LIKE ? AND
		id <> ? AND 
		(
			name LIKE ? OR 
			name LIKE ?
		)
	`).Find(&newTable)

	// SELECT * FROM products WHERE id IN (1,2,3);
	db.Find(&products, []int{1, 2, 3})

	// Gets all records.
	db.Find(&products)

	db.Find(&products, "code LIKE ?", "%5%")

	db.Find(&products, &Product{Code: "D43"})

	// String Conditions
	db.Where("code LIKE ? OR price = ?", "%42%", 120).Find(&products)

	db.Where("code = @code", map[string]interface{}{
		"code": "D45",
	}).Find(&products)

	// Struct Condition
	// SELECT * FROM products WHERE code = "D42" AND price = 100 ORDER BY id LIMIT 1;
	// NOTE When querying with struct, GORM will only query with non-zero fields, that means if your field’s value is 0, '', false or other zero values, it won’t be used to build query conditions.
	db.Where(&Product{Code: "D42", Price: 100}).First(&products)

	// db.Where(&Product{Code: "D42", Price: 0}).Find(&users)
	// The Price will be ignored due to its value is 0.
	// You can use map to build query conditions
	// SELECT * FROM products WHERE code = "D42" AND price = 0;
	db.Where(map[string]interface{}{"Code": "D42", "Price": 0}).Find(&products)

	// Not In
	// SELECT * FROM products WHERE code NOT IN ("D42", "D43");
	db.Not(map[string]interface{}{"code": []string{"D42", "D43"}}).Find(&products)

	// Defines anonymous structure slice and sets default value.
	//var p = []struct {
	//	code string
	//}{
	//	{code: "D44"},
	//	{code: "D45"},
	//}

	var prods []struct {
		Code string
	}

	//prods = append(prods, struct{ code string }{code: "D55"})

	db.Model(&Product{}).Select("code").Where("code LIKE ? OR price = ?", "%42%", 120).Find(&prods)

	var my []struct {
		Code  string
		Total int
	}

	// SELECT code, sum(price) as total FROM `products` GROUP BY `code` HAVING code = "group"
	db.Model(&Product{}).Select("code, sum(price) as total").Group("code").Having("code = ?", "D42").Find(&my)

	// Join example.
	//db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user)

	// You can use Joins eager loading associations with a single SQL, for example:
	// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id`;
	//db.Joins("Company").Find(&users)

	// Smart Select Fields
	//type APIUser struct {
	//	ID   uint
	//	Name string
	//}
	//
	//// Select `id`, `name` automatically when querying
	//// SELECT `id`, `name` FROM `users` LIMIT 10
	//db.Model(&User{}).Limit(10).Find(&APIUser{})

	// SubQuery
	//// SELECT * FROM "orders" WHERE amount > (SELECT AVG(amount) FROM "orders");
	//db.Where("amount > (?)", db.Table("orders").Select("AVG(amount)")).Find(&orders)
	//
	//// SELECT AVG(age) as avgage FROM `users` GROUP BY `name` HAVING AVG(age) > (SELECT AVG(age) FROM `users` WHERE name LIKE "name%")
	//subQuery := db.Select("AVG(age)").Where("name LIKE ?", "name%").Table("users")
	//db.Select("AVG(age) as avgage").Group("name").Having("AVG(age) > (?)", subQuery).Find(&results)

	// From SubQuery
	//// SELECT * FROM (SELECT `name`,`age` FROM `users`) as u WHERE `age` = 18
	//db.Table("(?) as u", db.Model(&User{}).Select("name", "age")).Where("age = ?", 18}).Find(&User{})
	//
	//// SELECT * FROM (SELECT `name` FROM `users`) as u, (SELECT `name` FROM `pets`) as p
	//subQuery1 := db.Model(&User{}).Select("name")
	//subQuery2 := db.Model(&Pet{}).Select("name")
	//db.Table("(?) as u, (?) as p", subQuery1, subQuery2).Find(&User{})

	// Named Argument
	//// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu"
	//db.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user)
	//
	//// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu" ORDER BY `users`.`id` LIMIT 1
	//db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu"}).First(&user)

	// Scopes allow you to specify commonly-used queries which can be referenced as method calls
	//func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
	//	return db.Where("amount > ?", 1000)
	//}
	//
	//func PaidWithCreditCard(db *gorm.DB) *gorm.DB {
	//	return db.Where("pay_mode_sign = ?", "C")
	//}
	//
	//func PaidWithCod(db *gorm.DB) *gorm.DB {
	//	return db.Where("pay_mode_sign = ?", "C")
	//}
	//
	//func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB {
	//	return func (db *gorm.DB) *gorm.DB {
	//		return db.Where("status IN (?)", status)
	//	}
	//}
	//
	//// Find all credit card orders and amount greater than 1000
	//db.Scopes(AmountGreaterThan1000, PaidWithCreditCard).Find(&orders)
	//
	//// Find all COD orders and amount greater than 1000
	//db.Scopes(AmountGreaterThan1000, PaidWithCod).Find(&orders)
	//
	//// Find all paid, shipped orders that amount greater than 1000
	//db.Scopes(AmountGreaterThan1000, OrderStatus([]string{"paid", "shipped"})).Find(&orders)

	// Count
	//var count int64
	//// SELECT count(1) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'
	//db.Model(&User{}).Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Count(&count)
}

func Update(db *gorm.DB) {
	// Saves all fields
	//db.First(&user)
	//
	//user.Name = "jinzhu 2"
	//user.Age = 100
	//
	//// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;
	//db.Save(&user)

}
